﻿using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using QLabValues;

#pragma warning disable 0659

namespace NDimArrayNamespace
{
    public class NDimArray<T> : ICloneable
    {
        // podaci
        private T[] _data;
        public int[] _dimensionSize;

        // properties
        public int Dimension
        { get { return _dimensionSize.Length; } }

        public int[] DimensionSize
        {
            get
            {
                return (int[])_dimensionSize.Clone();
            }
            set
            {
                _dimensionSize = (int[])value.Clone();
            }
        }

        public T[] Data
        {
            get
            {
                return (T[])_data.Clone();
            }
            set
            {
                _data = (T[])value.Clone();
            }
        }

        public T First
        {
            get
            {
                if (_data.Length > 0 && Dimension > 0)
                    return _data[0];
                else
                    throw new Exception("Empty array");
            }
        }

        public void SetDataElement(int index, T el)
        {
            if (index < ElementCount)
                _data[index] = el;
            else
                throw new Exception("Invalid index.");
        }

        public T this[params int[] coordinates]
        {
            get
            {
                // error checking
                if (coordinates.Length != Dimension)
                    throw new Exception("Dimension mismatch");
                for (int i = 0; i < Dimension; i++)
                    if (coordinates[i] + 1 > _dimensionSize[i])
                        throw new Exception("Dimension mismatch");

                // getting data
                /*
                if (_data[0] is ICloneable)
                {
                    var tmp = (ICloneable)_data[CoordinatesToPosition(coordinates)];
                    return (T)tmp.Clone();
                }
                else
                    return _data[CoordinatesToPosition(coordinates)];
                 */
                return _data[CoordinatesToPosition(coordinates)];
            }
            set
            {
                // error checking
                if (coordinates.Length < Dimension)
                {
                    var newcoord = new int[Dimension];

                    for (int i = 0; i < coordinates.Length; i++)
                        newcoord[i] = coordinates[i];

                    coordinates = newcoord;
                }

                // setting
                var doResize = false;

                if (coordinates.Length > Dimension)
                    doResize = true;

                for (int i = 0; i < Dimension; i++)
                    if (coordinates[i] + 1 > _dimensionSize[i])
                    { doResize = true; break; }

                // konstruisemo velicinu nove matrice
                if (doResize)
                {
                    var newDim = coordinates.Length > Dimension ? coordinates.Length : Dimension;
                    var newDimSize = new int[newDim];

                    for (int i = 0; i < Dimension; i++)
                        newDimSize[i] = coordinates[i] + 1 > _dimensionSize[i] ? coordinates[i] + 1 : _dimensionSize[i];
                    if (coordinates.Length > Dimension)
                        for (int i = Dimension; i < coordinates.Length; i++)
                            newDimSize[i] = coordinates[i] + 1;

                    Resize(newDimSize);
                }
                /*
                if (value is ICloneable)
                {
                    var tmp = (ICloneable)value;
                    _data[CoordinatesToPosition(coordinates)] = (T)tmp.Clone();
                }
                else
                    _data[CoordinatesToPosition(coordinates)] = value;
                 */
                _data[CoordinatesToPosition(coordinates)] = value;
            }
        }

        private int BlockSize
        { get { return _dimensionSize[0]; } }

        private int ElementCount
        { get { return _data.Length; } }

        private string _printIndexTree(NTree t)
        {
            if (t is Node) return _printIndexTreeNode((Node)t);
            else return _printIndexTreeLeaf((Leaf)t);
        }

        private string _printIndexTreeNode(Node n)
        {
            var sb = new StringBuilder();

            if (n.Level == 2)
            {
                for (int i = 0; i < n.ChildrenCount; ++i)
                {
                    int startIndex, endIndex;

                    Node tmp_n = (Node)n[i];
                    Leaf tmp_start = (Leaf)tmp_n[0];
                    Leaf tmp_end = (Leaf)tmp_n[tmp_n.ChildrenCount - 1];

                    startIndex = tmp_start.Index;
                    endIndex = tmp_end.Index + BlockSize;

                    sb.Append("[");
                    for (int col = 0; col < _dimensionSize[0]; ++col)
                    {
                        sb.Append("[");
                        for (int row = 0; row < _dimensionSize[1]; ++row)
                        {
                            int pos = startIndex + col + row * _dimensionSize[0];
                            //prilikom primene fje Resize moze se desiti da odredjeni elementi niza data budu null pa se zato ispituje != null
                            //takodje i ako se pozove konstruktor sa dimenzijama a bez Data argumenta, kreira se Data niz NULL elemenata odgovarajuce duzine
                            //npr QFloat64NDimArray(2, 3, 1, 4); ...
                            sb.Append(_data[pos] != null ? _data[pos].ToString().Replace(",", ".") : "0");
                            sb.Append(row == _dimensionSize[1] - 1 ? "" : ",");
                        }
                        sb.Append("]");
                    }
                    sb.Append("]");
                }
            }
            else if (n.Level == 1)
            {
                sb.Append("[");
                for (int col = 0; col < _dimensionSize[0]; ++col)
                {
                    sb.Append("[");
                    for (int row = 0; row < _dimensionSize[1]; ++row)
                    {
                        int pos = col + row * _dimensionSize[0];
                        //prilikom primene fje Resize moze se desiti da odredjeni elementi niza data budu null pa se zato ispituje != null
                        //takodje i ako se pozove konstruktor sa dimenzijama a bez Data argumenta, kreira se Data niz NULL elemenata odgovarajuce duzine
                        //npr QFloat64NDimArray(2, 3, 1, 4); ...
                        sb.Append(_data[pos] != null ? _data[pos].ToString().Replace(",", ".") : "0");
                        sb.Append(row == _dimensionSize[1] - 1 ? "" : ",");
                    }
                    sb.Append("]");
                    sb.Append(col == _dimensionSize[0] - 1 ? "" : "\n");
                }
                sb.Append("]");
            }
            else
            {
                sb.Append("[");
                for (int i = 0; i < n.ChildrenCount; ++i)
                    sb.Append(_printIndexTree(n[i]));
                sb.Append("]");
            }

            return sb.ToString();
        }

        private string _printIndexTreeLeaf(Leaf l)
        {
            var sb = new StringBuilder();

            sb.Append("[");
            for (int i = 0; i < BlockSize; ++i)
            {
                sb.Append(_data[l.Index + i].ToString().Replace(",", "."));
                sb.Append(i == BlockSize - 1 ? "" : ",");
            }
            sb.Append("]");

            return sb.ToString();
        }

        public override string ToString()
        {
            //prazan niz []
            if (ElementCount == 0)
            {
                //slucaj kada se stampa '[]'
                if (Dimension == 2 && DimensionSize[0] == 0 && DimensionSize[1] == 0)
                    return "[]";
                else
                //slucaj kada se stampa 'Empty matrix: m-by-0-by-n...'
                {
                    StringBuilder sb = new StringBuilder("Empty matrix: ");
                    sb.Append(DimensionSize[0]);
                    for (int i = 1; i < Dimension; i++)
                        sb.Append("-by-" + DimensionSize[i]);

                    return sb.ToString();
                }
            }
            else
            {
                var tree = _makeIndexTree();
                return _printIndexTree(tree);
            }
        }

        // konstruktori
        // konstruktor kreira prazan niz --> []
        public NDimArray()
        {
            _data = new T[0];
            _dimensionSize = new int[2];
            _dimensionSize[0] = _dimensionSize[1] = 0;
        }

        // za kreiranje praznog niza potrebno je da je najmanje jedna dimenzija jednaka nuli, npr. 2x0x4x3
        public NDimArray(params int[] dimensionSize)
        {
            _dimensionSize = (int[])dimensionSize.Clone();

            int dataSize = 1;
            foreach (var x in _dimensionSize)
            {
                if (x < 0)
                    throw new Exception("Invalid dimension size");
                dataSize *= x;
            }

            _data = new T[dataSize];
        }

        public NDimArray(T[] data, params int[] dimensionSize)
        {
            // inicijalne provere
            int dataSize = 1;
            foreach (var x in dimensionSize)
            {
                if (x < 0)
                    throw new Exception("Invalid dimension size");
                dataSize *= x;
            }

            //konstruktor ce se koristiti da se od postojeceg NDimArray-a kreira novi NDimArray pa ce zato uvek biti data != null
            if (dataSize != data.Length)
                throw new Exception("Invalid data array size");

            // konstrukcija
            _dimensionSize = (int[])dimensionSize.Clone();
            _data = (T[])data.Clone();
        }

        // metod za kopiranje
        public object Clone()
        {
            return (object)new NDimArray<T>(_data, _dimensionSize); // svo kopiranje se vec vrsi u konstruktoru
        }

        // javni metodi
        public NDimArray<T> GetSubNDimArray(int[][] subNDimArrayCoordinates)
        {
            // provere se vrse u konstruktori za slice


            // netko: specijalan slucaj kada je prosledjen samo jdan argument, cela matrica se tretira kao jednodimenzioni niz _data
            if (subNDimArrayCoordinates.Count() == 1)
            {
                var ret1 = new NDimArray<T>(new int[] { subNDimArrayCoordinates[0].Count() });
                for (int i = 0; i < subNDimArrayCoordinates[0].Count(); i++)
                {
                    if ((subNDimArrayCoordinates[0][i] < 0) || (subNDimArrayCoordinates[0][i] >= this._data.Count()))
                        throw new NumericalDataRepException("Index exceeds matrix dimensions.");
                    ret1._data[i] = this._data[subNDimArrayCoordinates[0][i]];
                }
                return ret1;
            }

            var slice = new NDimArraySlice<T>(this, subNDimArrayCoordinates);

            var ret = new NDimArray<T>(slice.DimensionSize);


            for (int i = 0; i < slice.ElementCount; ++i)
                ret._data[i] = slice[slice.PositionToCoordinates(i)];

            return ret;
        }

        public NDimArray<T> SubscriptedAssignment(NDimArray<T> array, int[][] coordinates)
        {
            //TODO: ubaciti provere!!!
            //TODO: ubaciti resizing slucaj!!!

            // TODO: specijalan slucaj kad imamo samo jedan argument (pogledati metod iznad...)

            bool doResize = Dimension < coordinates.Length;

            var maxCoords = new int[coordinates.Length];

            for (int i = 0; i < maxCoords.Length; ++i)
            {
                maxCoords[i] = coordinates[i].Max();
                doResize = doResize || (maxCoords[i] + 1) > _dimensionSize[i];
            }

            var ret = (NDimArray<T>)this.Clone();

            if (doResize)
            {
                var count = maxCoords.Length < Dimension ? Dimension : maxCoords.Length;
                var resizeCoord = new int[count];
                for (int i = 0; i < resizeCoord.Length; ++i)
                {
                    if (i < maxCoords.Length)
                        resizeCoord[i] = maxCoords[i] > DimensionSize[i] - 1 ? maxCoords[i] + 1 : DimensionSize[i];
                    else if (i >= maxCoords.Length && i < Dimension)
                        resizeCoord[i] = DimensionSize[i];
                    else
                        resizeCoord[i] = 1;
                }
                ret.Resize(resizeCoord);
            }

            var slice = new NDimArraySlice<T>(ret, coordinates);

            for (int i = 0; i < slice.ElementCount; ++i)
                slice[slice.PositionToCoordinates(i)] = array[slice.PositionToCoordinates(i)];

            return ret;
        }

        public override bool Equals(object obj)
        {
            if (obj == null || GetType() != obj.GetType())
                return false;

            var ndimar = (NDimArray<T>)obj;

            if (this.ElementCount != ndimar.ElementCount)
                return false;

            if (this.Dimension != ndimar.Dimension)
                return false;

            for (int i = 0; i < this.Dimension; i++)
                if (_dimensionSize[i] != ndimar._dimensionSize[i])
                    return false;

            for (int i = 0; i < this.ElementCount; i++)
                if (!_data[i].Equals(ndimar._data[i]))
                    return false;

            return true;
        }

        // staticki metodi
        public static NDimArray<T> Concatenate(int direction, params NDimArray<T>[] array)
        {
            // provera ispravnosti dimenzija
            for (int i = 1; i < array.Length; i++)
                if (array[0].Dimension != array[i].Dimension)
                    throw new Exception("Dimension mismatch");

            for (int i = 1; i < array.Length; i++)
                for (int j = 0; j < array[0]._dimensionSize.Length; j++)
                    if (j != direction)
                        if (array[0]._dimensionSize[j] != array[i]._dimensionSize[j])
                            throw new Exception("Dimension mismatch");

            // konkatenacija, treba da radi i u slucaju kada je direction > Dimension

            // racunamo dimenzije nove matrice
            int newNDimArrayDimension;
            if (direction + 1 > array[0].Dimension)
                newNDimArrayDimension = direction + 1;
            else
                newNDimArrayDimension = array[0].Dimension;

            var newNDimArrayDimensionSize = new int[newNDimArrayDimension];

            for (int i = 0; i < newNDimArrayDimension; i++)
                if (i != direction && i < array[0].Dimension)
                    newNDimArrayDimensionSize[i] = array[0].DimensionSize[i];

            if (direction + 1 > array[0].Dimension)
            {
                for (int i = array[0].Dimension; i < newNDimArrayDimension; i++)
                    newNDimArrayDimensionSize[i] = 1;
                newNDimArrayDimensionSize[newNDimArrayDimension - 1] = array.Length;
            }
            else
            {
                newNDimArrayDimensionSize[direction] = 0;
                for (int i = 0; i < array.Length; i++)
                    newNDimArrayDimensionSize[direction] += array[i]._dimensionSize[direction];
            }

            // konstruisemo novu matricu
            var catNDimArray = new NDimArray<T>(newNDimArrayDimensionSize);

            // kopiramo elemente u novu matricu
            var chunks = new int[array.Length];

            if (direction + 1 > array[0].Dimension)
            {
                for (int i = 0; i < chunks.Length; i++)
                    chunks[i] = array[0].ElementCount;
            }
            else
            {
                int tmp = 1;
                for (int i = 0; i < direction; i++)
                    tmp *= array[0]._dimensionSize[i];
                for (int i = 0; i < chunks.Length; i++)
                    chunks[i] = tmp * array[i]._dimensionSize[direction];
            }

            int reschunks = 0;
            foreach (var x in chunks)
                reschunks += x;

            var count = catNDimArray.ElementCount / reschunks;

            for (int i = 0; i < count; i++)
            {
                int offset = 0;

                for (int j = 0; j < array.Length; j++)
                {
                    ArrayMemCopy(catNDimArray._data, array[j]._data, i * reschunks + offset, i * chunks[j], chunks[j]);
                    offset += chunks[j];
                }
            }

            return catNDimArray;
        }

        // pomocni metodi
        private int CoordinatesToPosition(params int[] coordinates)
        {
            int retval = coordinates[Dimension - 1];

            for (int i = Dimension - 2; i > -1; i--)
                retval = retval * _dimensionSize[i] + coordinates[i];

            return retval;
        }

        private int[] PositionToCoordinates(int position)
        {
            var coordinates = new int[Dimension];

            var rest = position;

            for (int i = 0; i < Dimension; i++)
            {
                coordinates[i] = rest % _dimensionSize[i];
                rest = rest / _dimensionSize[i];
            }

            return coordinates;
        }

        // kopira do length elemenata niza src u niz dst
        private static void ArrayMemCopy(T[] dst, T[] src, int startDst, int startSrc, int length)
        {
            // provera dimenzija
            if (dst.Length < startDst && src.Length < startSrc)
                return;

            for (int i = 0; i < length; i++)
            {
                if ((startSrc + i) >= src.Length && (startDst + i) >= dst.Length)
                    break;

                /*
                if (dst[0] is ICloneable) // postizem li ovim pravo kopiranje elemenata?
                {
                    var x = (ICloneable)src[startSrc + i];
                    dst[i] = (T)x.Clone();
                }
                else
                {
                    dst[startDst + i] = src[startSrc + i]; // ovde bi trebalo kopirati
                }
                */
                dst[startDst + i] = src[startSrc + i];
            }
        }

        private void Resize(params int[] newDimensions)
        {
            // alociramo prostor
            var newNDimArray = new NDimArray<T>(newDimensions);

            // kopiramo podatke
            for (int i = 0; i < this.ElementCount; i++)
            {
                var coordinates = PositionToCoordinates(i);
                if (_data[0] is ICloneable)
                {
                    var tmp = (ICloneable)_data[i];
                    newNDimArray[coordinates] = (T)tmp.Clone();
                }
                else
                    newNDimArray[coordinates] = _data[i];
            }

            // postavljamo nove elemente matrice

            _data = newNDimArray._data;
            _dimensionSize = newNDimArray._dimensionSize;
        }

        private NTree _makeIndexTree()
        {
            NTree _indexTree;

            var stack1 = new Stack<NTree>();
            var stack2 = new Stack<NTree>();

            // drvo se gradi od dna 
            var count = _data.Length / BlockSize;

            for (int i = count - 1; i > -1; i--)
                stack1.Push(new Leaf(i * BlockSize));

            for (int i = 1; i < _dimensionSize.Length; i++)
            {
                while (stack1.Count > 0)
                {
                    var l = new List<NTree>();
                    for (int j = 0; j < _dimensionSize[i]; j++)
                        l.Add(stack1.Pop());
                    stack2.Push(new Node(i, l.ToArray()));
                }

                while (stack2.Count > 0)
                    stack1.Push(stack2.Pop());
            }

            _indexTree = stack1.Pop();

            return _indexTree;
        }

        // pomocne klase
        private abstract class NTree
        { }

        private class Node : NTree
        {
            private NTree[] _children;

            public int Level { get; private set; }

            public int ChildrenCount
            { get { return _children.Length; } }

            public NTree this[int index]
            { get { return _children[index]; } }

            public Node(int level, params NTree[] children)
                : base()
            {
                _children = children;
                Level = level;
            }

            public override bool Equals(object obj)
            {
                if (obj == null || GetType() != obj.GetType())
                    return false;

                var node = (Node)obj;

                if (_children.Length != node._children.Length)
                    return false;

                for (int i = 0; i < _children.Length; i++)
                    if (!_children[i].Equals(node._children[i]))
                        return false;

                return true;
            }
        }

        private class Leaf : NTree
        {
            private int _index;

            public int Index
            { get { return _index; } }

            public Leaf(int index)
                : base()
            { _index = index; }

            public override bool Equals(object obj)
            {
                if (obj == null || GetType() != obj.GetType())
                    return false;

                var leaf = (Leaf)obj;

                return (_index == leaf._index);
            }
        }

        public bool IsComplex()
        {
            throw new NotImplementedException();
        }


        public NDimArray<T> OneDimArray()
        {
            // TODO: od n dimenzija pravi samo jednu
            throw new System.NotImplementedException();
        }
    }

    public class NDimArraySlice<T>
    {
        private int[] _dimensionSize;
        private int[][] _coordinateMap;
        private NDimArray<T> _array;

        public int Dimension
        { get { return _dimensionSize.Length; } }

        public int[] DimensionSize
        { get { return _dimensionSize; } }

        public int ElementCount
        {
            get
            {
                if (_dimensionSize.Length == 0)
                    return 0;

                int count = 1;

                foreach (int x in _dimensionSize)
                {
                    count *= x;
                }
                return count;
            }
        }

        public T this[params int[] coordinates]
        {
            get
            {
                // error checking
                if (coordinates.Length != Dimension)
                    throw new Exception("Dimension mismatch");
                for (int i = 0; i < Dimension; i++)
                    if (coordinates[i] + 1 > _dimensionSize[i])
                        throw new Exception("Dimension mismatch");

                return _array[MapCoordinate(coordinates)];
            }
            set
            {
                // error checking
                if (coordinates.Length != Dimension)
                    throw new Exception("Dimension mismatch");
                for (int i = 0; i < Dimension; i++)
                    if (coordinates[i] + 1 > _dimensionSize[i])
                        throw new Exception("Dimension mismatch");

                _array[MapCoordinate(coordinates)] = value;
            }
        }

        public NDimArraySlice(NDimArray<T> array, int[][] coordinateMap)
        {
            if (coordinateMap.Length > array.Dimension)
                throw new Exception("Invalid dimension.");

            var dimensionSize = array.DimensionSize;
            var count = dimensionSize.Length > coordinateMap.Length ? coordinateMap.Length : dimensionSize.Length;

            for (int i = 0; i < count; ++i)
            {
                var tmp = coordinateMap[i];

                for (int j = 0; j < tmp.Length; ++j)
                {
                    if (tmp[j] >= dimensionSize[i])
                        throw new NumericalDataRepException("Index exceeds matrix dimensions.");
                }
            }

            _coordinateMap = new int[dimensionSize.Length][];

            for (int i = 0; i < count; ++i)
            {
                if (coordinateMap[i].Length != 0)
                    _coordinateMap[i] = coordinateMap[i];
                else
                {
                    _coordinateMap[i] = new int[dimensionSize[i]];
                    for (int j = 0; j < _coordinateMap[i].Length; ++j)
                        _coordinateMap[i][j] = j;
                }
            }

            _dimensionSize = new int[dimensionSize.Length];

            for (int i = 0; i < _dimensionSize.Length; ++i)
                _dimensionSize[i] = _coordinateMap[i].Length;

            _array = array;
        }

        public int[] PositionToCoordinates(int position)
        {
            if (position < 0 || position >= this.ElementCount)
                throw new Exception("Invalid position");
            var coordinates = new int[Dimension];

            var rest = position;

            for (int i = 0; i < Dimension; i++)
            {
                coordinates[i] = rest % _dimensionSize[i];
                rest = rest / _dimensionSize[i];
            }

            return coordinates;
        }

        private int[] MapCoordinate(int[] coordinate)
        {
            var ret = new int[coordinate.Length];

            for (int i = 0; i < coordinate.Length; ++i)
                ret[i] = _coordinateMap[i][coordinate[i]];

            return ret;
        }
    }


}
